﻿/*
    Copyright (c) 2010 OpenNETCF Consulting, LLC

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
    and associated documentation files (the "Software"), to deal in the Software without restriction, 
    including without limitation the rights to use, copy, modify, merge, publish, distribute, 
    sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all copies or 
    substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 
    BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
    DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Diagnostics;

namespace OpenNETCF.Barcode.Decoders
{
    internal abstract class BaseDecoder : IDecoder
    {
        private Bitmap m_image;
        private Rectangle m_bounds;

        public abstract Barcode Decode();
        public abstract byte GetRecommendedThreshold();

        public byte Threshold { get; set; }

        // replace this with intelligence
        public int ScanY { get; set; }

        public Bitmap Image 
        {
            get { return m_image; }
            set
            {
                m_image = value;
            }
        }

        public void SetImage(Bitmap image)
        {
            Image = image;
        }

        public Rectangle BarcodeBounds
        {
            private set { m_bounds = value; }
            get 
            {
                    m_bounds = GetBounds();

                return m_bounds; 
            }
        }

        private Rectangle GetBounds()
        {
            int top = 0;
            int bottom = 0;
            int left = 0;
            int right = 0;

            var middle = Image.Height / 2;

            var lums = GetBinaryLuminanceLine(middle, 0);
            GetBarcodeSides(lums, out left, out right);

            var fft = new FFTReal(2048);
            var res = fft.Compute(lums);

            // save the top 5 frequencies
            var baseline = new Spectrum(fft.GeneratePowerSpectrum(res, lums.Length, SortOrder.Descending), 5);


            // find the top
            int y = middle - 10;
            while (y > 0)
            {
                double[] test = new double[5];

                lums = GetBinaryLuminanceLine(y, 0);
                res = fft.Compute(lums);
                var spectrum = new Spectrum(fft.GeneratePowerSpectrum(res, lums.Length, SortOrder.Descending), 5);

                if (spectrum.IsSimilarTo(baseline, 4, 90))
                {
                    Debug.Write("SAME");
                }
                else
                {
                    Debug.Write("DIFFERENT");
                    top = y + 10;
                    break;
                }

                Debug.Write("\r\n");

                y -= 10;
            }

            // find the bottom
            y = middle + 10;
            while (y < Image.Height)
            {
                double[] test = new double[5];

                lums = GetBinaryLuminanceLine(y, 0);
                res = fft.Compute(lums);
                var spectrum = new Spectrum(fft.GeneratePowerSpectrum(res, lums.Length, SortOrder.Descending), 5);

                if (spectrum.IsSimilarTo(baseline, 4, 90))
                {
                    Debug.Write("SAME");
                }
                else
                {
                    Debug.Write("DIFFERENT");
                    bottom = y - 10;
                    break;
                }
                Debug.Write("\r\n");

                y += 10;
            }

            return new Rectangle(left, top, right - left, bottom - top);
        }

        public byte[] GetLuminanceLine(int ypos, int angle, bool useScaling)
        {
            if (useScaling)
            {
                return GetScaledLuminanceLine(ypos, angle);
            }

            return GetUnfilteredLuminanceLine(ypos, angle);
        }

        public byte[] GetBinaryLuminanceLine(int ypos, int angle)
        {
            List<byte> lums = new List<byte>();

            var data = GetLuminanceLine(ypos, angle, true);

            for (int x = 0; x < data.Length; x++)
            {
                byte value;
                value = (byte)((data[x] > Threshold) ? byte.MinValue : byte.MaxValue); 
                lums.Add(value);
            }
            return lums.ToArray();
        }

        internal byte[] GetScaledLuminanceLine(int ypos, int angle)
        {
            if ((ypos < 0) || (ypos > Image.Height)) throw new ArgumentOutOfRangeException("ypos");

            // TODO:
            if (angle != 0) throw new NotSupportedException();

            List<byte> lums = new List<byte>();
            byte min = byte.MaxValue;
            byte max = byte.MinValue;

            byte lastValue = 0;
            byte nextValue = 0;
            byte currentValue = 0;
            byte value = 0;

            // TODO: change to use LockBits for speed improvement
            for (int x = 0; x < Image.Width; x++)
            {
                Color color;

                // rolling average to get rid of peaks and valleys at edges of bars
                if (x == 0)
                {
                    // first bit
                    color = Image.GetPixel(x, ypos);
                    currentValue = color.GetLuminance();
                    var nextColor = Image.GetPixel(1, ypos);
                    nextValue = nextColor.GetLuminance();

                    value = (byte)((currentValue + nextValue) / 2);
                }
                else if (x < Image.Width - 1)
                {
                    color = Image.GetPixel(x + 1, ypos);
                    // shift the values
                    lastValue = currentValue;
                    currentValue = nextValue;
                    nextValue = color.GetLuminance();

                    value = (byte)((lastValue + currentValue + nextValue) / 3);
                }
                else
                {
                    // last bit
                    lastValue = currentValue;
                    currentValue = nextValue;
                    value = (byte)((lastValue + currentValue) / 2);
                }

                lastValue = value;

                if (value > max) max = value;
                if (value < min) min = value;
                lums.Add(value);

            }

            // re-scale the values so 0 == minimum luminance and 255 == maximum
            float range = max - min;
            float factor = byte.MaxValue / range;

            var newList = (from l in lums
                           select (byte)((l - min) * factor)).ToArray();

            return newList;
        }

        internal byte[] GetUnfilteredLuminanceLine(int ypos, int angle)
        {
            List<byte> lums = new List<byte>();

            // TODO: change to use LockBits for speed improvement
            for (int x = 0; x < Image.Width; x++)
            {
                var color = Image.GetPixel(x, ypos);
                lums.Add(color.GetLuminance());
            }
            return lums.ToArray();
        }

        public byte[] ConvertToBinary(byte[] luminanceData, byte threshold)
        {
            byte[] output = new byte[luminanceData.Length];

            for (int i = 0; i < luminanceData.Length; i++)
            {
                output[i] = (luminanceData[i] >= threshold) ? byte.MinValue : byte.MaxValue;
            }

            return output;
        }

        /// <summary>
        /// Finds the offset of the next transition (low to high or high to low)
        /// </summary>
        /// <param name="data"></param>
        /// <param name="startOffset"></param>
        /// <returns></returns>
        protected int NextTransition(byte[] data, int startOffset)
        {
            bool initialState = data[startOffset] == byte.MaxValue;
            bool currentState;
            int position = startOffset;
            do
            {
                position++;
                currentState = (data[position] == byte.MaxValue);
            } while (currentState == initialState);

            return position;
        }

        /// <summary>
        /// Gets the width of the bit by looking at the width of the first 3 transitions and averaging them
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        protected int GetDigitWidthFromDelimiterSize(byte[] data)
        {
            List<int> widths = new List<int>();

            int start = 0;

            for (int i = 0; i < 3; i++)
            {
                // look for the first "on"
                start = NextTransition(data, start);
                int end = NextTransition(data, start) - 1;

                widths.Add(end - start);
                start = end;
            }

            var avg = (int)widths.Average();
            Debug.WriteLine("Width = " + avg.ToString());
            return avg;
        }

        /// <summary>
        /// Finds the left and right bounds of the barcode
        /// </summary>
        /// <param name="data"></param>
        /// <param name="left"></param>
        /// <param name="right"></param>
        protected void GetBarcodeSides(byte[] data, out int left, out int right)
        {
            left = NextTransition(data, 0);
            right = NextTransition(data.Reverse().ToArray(), 0);
            right = data.Length - right - 1;
        }

        /// <summary>
        /// Gets the width of a bit by dividing the width of the entire barcode by the number of bits expected in teh barcode
        /// </summary>
        /// <param name="data"></param>
        /// <param name="totalBits"></param>
        /// <returns></returns>
        protected int GetDigitWidthFromBounds(byte[] data, int totalBits)
        {
            int left, right;
            GetBarcodeSides(data, out left, out right);

            return (right - left) / totalBits;
        }

        /// <summary>
        /// Gets a bit value (0 or 1) for a set of luminance values.  This is done by averaging the luminance over the width then
        /// checking to see if that average is above or below the given threshold
        /// </summary>
        /// <param name="data"></param>
        /// <param name="offset"></param>
        /// <param name="width"></param>
        /// <param name="threshold"></param>
        /// <returns></returns>
        protected int GetBit(byte[] data, int offset, int width, int threshold)
        {
            int sum = 0;

            for (int i = offset; i < offset + width; i++)
            {
                sum += data[i];
            }

            // if the average luminance across the segment is > threshold, then 0, else 1
            return ((sum / width) > Threshold) ? 0x01 : 0x00;
        }
    }
}
